Skip to content

Conversation

@anxi01
Copy link
Member

@anxi01 anxi01 commented Nov 3, 2024

개발파트 심사위원 코드리뷰용 PR입니다.

바쁜 와중에 코드 리뷰해 주셔서 정말 감사드립니다.

Copy link

@devxb devxb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클린아키텍처, DDD와 같은 개념이 쉽지 않았을거 같은데, 여러가지 시도를 해주신것 같아서 재미있게 봤습니다. 수고하셨습니다 👍

질문 주신내용중 하나인, 3. 프로젝트의 품질을 향상하고 싶습니다. 에 대해서는 여기에 답해보도록 할게요.

사실 어떤 방향으로의 품질 향상인지가 약간 추상적이라 생각나는대로 적어보겠습니다 🙇‍♂️

  • 이미 알고계실수도 있을거 같은데, AggregateRoot, Domain service, Application Service 에 대해서 공부해보셔도 좋을거 같습니다.

  • 클린아키텍처가 과연 좋을까요?

  • 테스트작성 방법중에서 Describe-Context-It 이라는 패턴이 있습니다. DDD를 하게 되면 도메인이 비대해지면서 테스트코드도 하나의 클래스에 몰려있는 경우가 발생하는데요. 이때, Describe-Context-It을 쓰면 늘어나는 테스트코드를 관리하기 쉬워집니다.
    참고: https://johngrib.github.io/wiki/junit5-nested/

  • sonar cloud를 쓰면 lint를 어느정도 자동화 해서 관리해줍니다. https://www.sonarsource.com/products/sonarcloud/

  • application service를 활용하다보면 모든 로직을 트랜잭션으로 묶지 못하는 경우가 발생하는데요. 이때, 업데이트를 일부만 성공하게 된다면 데이터 정합성은 깨지게 됩니다. 이런 상황을 어떻게 방지할 수 있을까요?

  • 각 도메인 끼리는 어떻게 호출을 해야할까요?

  • API가 중복(따닥) 호출되면 어떻게 될까요? 어떻게 멱등한 API를 구현할 수 있을까요?

  • 일단 유저를 모으는게 중요한것 같아요. (안된다면 기술을 해야겠지만요) 같은 개선을 하더라도 유저관점에서 유의미한 개선을 했을때 오는 임팩트가 크다고 생각합니다.

더 많을거 같은데 당장 생각나는건 이정도네요 ㅎㅎ

Dockerfile-local Outdated

EXPOSE 8080
ENTRYPOINT ["java"]
CMD ["-jar", "backend.jar"] No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소한것이지만 파일의 마지막에 개행을 넣으시면 아래 사진과 같은것을 안 볼 수 있습니다!
스크린샷 2024-11-06 오후 10 16 22

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다!!

build.gradle Outdated
Comment on lines 26 to 59
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// WEB
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'

// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'com.h2database:h2'

// Development tools (for local development only)
developmentOnly 'org.springframework.boot:spring-boot-devtools'

// DB, JPA
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Lombok (code simplification)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Validation (for request/response validation)
implementation 'org.springframework.boot:spring-boot-starter-validation'

// WebSocket (for real-time communication)
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Swagger-UI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 사소한것이지만, 모든 설정을 하나의 gradle파일에 두면, 프로젝트가 커졋을때 관리하기 힘들어집니다.

아래 사진과 같이, 역할별로 gradle파일을 만들고 (메인)build.gradle에 apply를 해주면 관리하기 편해지니 시도해보셔도 좋을거 같습니다.
참고용 레포지토리도 링크로 남겨드립니다

스크린샷 2024-11-06 오후 10 18 07

<build.gradle>
apply from: "gradle/db.gradle"
apply from: "gradle/jwt.gradle"
apply from: "gradle/core.gradle"
apply from: "gradle/test.gradle"
apply from: "gradle/slack.gradle"
apply from: "gradle/spring.gradle"
apply from: "gradle/monitor.gradle"
apply from: "gradle/jetbrains.gradle"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 적용해보겠습니다! 감사합니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 팁 감사합니다!!

build.gradle Outdated
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// WebFlux 외부 API 호출
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mvc와 webflux를 함께 쓰시나요?

  • 이러면 서버는 Tomcat으로 뜨게될까요 Netty로 뜨게될까요?
  • mvc와 webflux는 어떤상황에 같이 사용할 수 있을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 저희 서비스는 WebFlux를 활성화하지 않았기에, Tomcat으로 서버가 구동됩니다.
WebFlux를 추가한 이유는 외부 API(OpenAI, ClovaStudio)를 사용하려고 했기에 추가했습니다!

@Service
@RequiredArgsConstructor
@Log4j2
@Transactional
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 DDD를 좋아하는데, 저는 트랜잭션 관리의 용이성 때문에 domain Service는 domain레이어에 구현하는걸 추천드립니다. (자연스럽게 @Transactional도 domain 밖으로 빠져나오지 않겠죠?)
application 레이어에서는 여러 domain service 를 조합해서 사용해야 하거나 I/O 작업이 발생하는데, 트랜잭션 안에는 넣으면 안되는 상황에 사용합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • application service : 여러개의 도메인 서비스의 오케스트레이션 및 트랜잭션분리
  • domain service : 각 도메인 특정한 로직을 담당

import jakarta.persistence.Enumerated;
import lombok.*;

@Embeddable
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클린아키텍처를 사용한것은 아닌가요?
domain layer가 POJO로 되어있고 의존성도 application -> domain <- infra 로 가고있는거 같아서 클린아키텍처 + DDD라고 생각했는데 여기 jakarta쪽 어노테이션이 있군요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클린아키텍처를 사용하려고 하였으나, 잘 모르는 상황이었어서 사용하지 않았습니다..!
그대신 표현 -> 응용 -> 도메인 <- 인프라 4계층으로 DDD를 설계하려고 하였습니다!

도메인 계층은 POJO로 구현하고 인프라에서 JPA를 사용하였지만,
밸류 타입의 경우 도메인 계층에서 jakarta 어노테이션을 사용하여 POJO로 생성하지 못했습니다..

밸류 타입도 도메인은 POJO로 변경하고, 인프라에 jakarta 어노테이션 사용 및 mapper를 통해 관리하도록 변경하겠습니다!
감사합니다~

Comment on lines 9 to 10
import spring.backend.activity.query.dao.ActivityDao;
import spring.backend.activity.query.dao.QuickStartDao;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

member -> activity로 의존성이 가고있네요 ㅎㅎ
DDD를 사용해서 도메인별로 로직을 잘 분리해주어도. 사용하는쪽의 의존성을 관리하지 않으면 관리하기 힘든 코드가 될 수 있습니다! (지금 그렇다는건 아니에요)

다른 도메인들끼리는 어떤 방식으로 호출을하는게 좋을지(네트워크 호출? 코드 호출?), 레이어는 어디끼리 참조하고 있어야 좋을지 고민해보셔도 좋을거 같아요.
ex. member.application -> activity.domain?
ex. member.application -> activity.application?

Comment on lines 35 to 39
QuickStartRequest request = new QuickStartRequest(null, 12, 30, "오전", 300, Type.OFFLINE);
Set<ConstraintViolation<QuickStartRequest>> violations = validator.validate(request);

assertThat(violations).isNotEmpty();
assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 필수 입력 항목입니다."));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given, when, then을 주석으로라도 명시해놓는게 추후 코드를 다시 읽거나 다른사람이 테스트 코드를 파악할때 도움이 됩니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쓰다 안쓰다 했는데 확실하게 써주는게 좋겠네요!
감사합니다 ㅎㅎ

private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :");
private static final String LINE_SEPARATOR = "\n";
private final RecommendationProvider recommendationProvider;
public List<ClovaRecommendationResponse> getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 함수위에 개행이 빠져있는것 같아요 ㅎㅎ

@Service
@RequiredArgsConstructor
@Log4j2
public class ClovaService {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service라는 네이밍은 통상적으로 어떤 역할을할때 사용되고 있을지 고민해봐도 좋을거 같습니다.

Copy link
Contributor

@HoyeongJeon HoyeongJeon Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service 네이밍은 비즈니스 로직의 중심이 되는 계층이나 애플리케이션의 핵심 기능을 구현하고 관리하는 역할이라 알고있습니다.

코드를 작성하던 당시 추천 목록을 받아오니 핵심 기능을 구현한다 생각해 ClovaService라 지었습니다.

현재 저희 애플리케이션의 메인 로직은 추천 목록을 받아오는 것이 아닌, 받아온 추천 목록을 파싱해 사용자에게 전달하는 것이기에 Service가 아닌 RecommendationProvider로 이름을 변경했습니다.

더 깊게 생각할거리를 주셔서 감사합니다.

Comment on lines 28 to 34
return clovaStudioWebClient.post()
.uri(apiUrl)
.bodyValue(request)
.retrieve()
.bodyToMono(ClovaResponse.class)
.block();
} catch (WebClientException e) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 block을 걸면 reactive의 이점을 누릴 수 있을까요?

reactive가 항상 빠른것은 아니며, 잘못사용하면 오히려 느려지는 경우가 많은데요. 가장 많이 하는 실수중 하나가, reactive 코드에서 blocking I/O를 유발하는 작업을 할때 인 것 같아요.

reactive가 어떻게 성능적인 이점을 주는지, 알아보셔도 좋을거 같아요.

HoyeongJeon and others added 29 commits November 10, 2024 01:02
@anxi01 anxi01 deleted the branch review June 30, 2025 18:43
@anxi01 anxi01 closed this Jun 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants